import java.net.*;
import java.util.*;
import java.io.*;

public class Client {

	/**
	 * @param 
	 * arg0:IP address of controller, 
	 * arg1:Port of controller
	 * arg2:IP address of standby controller, 
	 * arg3:Port of standby controller
	 */
	public static void main(String[] args) {

		InetAddress addr;
		int port;
		List<Address> neighbors = new ArrayList<Address>(); //
		
		//Controller information
		Socket controller;
		BufferedReader in;
		PrintWriter out;
		
		int nodeID = -1; //the Id of the client (the controller provides it)
		int state = 0; //Current state of client (how many messages it got)
		
		String responseLine;	
		ServerSocket ss = null;
		boolean recovering = false;
		
		try {
			 ss = new ServerSocket(0); //To connect to another client / Random socket
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		try {
			
			// 1) Connect with controller
			addr = InetAddress.getByName(args[0]); //IP @ of controller
			port = Integer.parseInt(args[1]);
			System.out.println("addr = " + addr);	
			
			try{
				controller = new Socket(addr, port); //Create a socket to the controller (address , port)
			}
			catch (ConnectException e) {
				//Controller has crashed, change to standby controller
				addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
				port = Integer.parseInt(args[3]);
									
				controller = new Socket(addr, port);
			}
			
			//controller = new Socket(addr, port); //Create a socket to the controller (address , port)			
			System.out.println("socket = " + controller);
			in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
			out = new PrintWriter(controller.getOutputStream(),true); //Send to controller
			
			File file = new File("file.txt"); //if file exists means that the node is recovering
			String strLine = null;
			if(file.exists()){
				BufferedReader br = new BufferedReader(new InputStreamReader(new DataInputStream(new FileInputStream(file))));
				strLine = br.readLine();
				System.out.println("Read from file" + strLine);
				out.println("recover " + strLine + " " + ss.getLocalPort()); //send the socket that this client listens to the controller
				br.close();
				recovering = true;
				nodeID = Integer.parseInt(strLine);
			}
			else{
				out.println("port " + ss.getLocalPort()); //send the socket that this client listens to the controller
			}
			
			//Get node id from controller (acts as an ACK too)
			//try catch controller recv failed
			String[] line;
			int wait = 5000;
			while(true){
				try{					
					responseLine = in.readLine();
					line = responseLine.split(" ");
					break;
				}
				catch (SocketException e) {
					Thread.sleep(wait);
					controller = new Socket(addr, port); //Create a socket to the controller (address , port)			
					System.out.println("Socket reinit");
					in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
					wait += 500;
				}
			}
			
			if(responseLine.equals("change")){ //get message from controller about sending the message to the other controller
				System.out.println("CHANGE");
				if(port == Integer.parseInt(args[1])){
					addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
					port = Integer.parseInt(args[3]);
				}
				else{
					addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
					port = Integer.parseInt(args[1]);
				}						
				controller = new Socket(addr, port);
				System.out.println("Connected " + controller);
				out = new PrintWriter(controller.getOutputStream(),true); //Send to controller
				in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
				out.println("recover " + strLine + " " + ss.getLocalPort()); //send the socket that this client listens to the controller
				responseLine = in.readLine();
				line = responseLine.split(" ");
			}
			
			if(line[0].equals("recover")){ //get message from controller about the recovering state
				state = Integer.parseInt(line[1]);
				out.println("ACK");
				System.out.println("Node recovered");
			}
			else{ //if simple message with nodeID received
				nodeID = Integer.parseInt(responseLine);
				BufferedWriter output = new BufferedWriter(new FileWriter("file.txt")); //write to file the node id
				output.write("" + nodeID);
				output.close();
				System.out.println("Node id: " + nodeID);
				System.out.println("LocalPort of Node " + nodeID + " is " + ss.getLocalPort());
			}
						
			// 2) Wait and get all other clients' data			
			while (true){
				responseLine = in.readLine();
				System.out.println(responseLine);
				if (responseLine.equals("done")) 
					break;
				
				String res[] = responseLine.split(" ");
				neighbors.add(new Address(res[0], Integer.parseInt(res[1]),Integer.parseInt(res[2]))); // store it in neighbors list		
		    }
			
			// 3) Sending messages		
			Socket neighborSock = null;
			PrintWriter outSock = null;
			BufferedReader inSock = null;
			boolean hasPotato = false;
			int prevCrashed = -1;
			int recoveredNode = -1;
			boolean timeout = false;
			
			// If client is 0, begin sending msg
			while(nodeID == 0 && !recovering){ 
				int index = -1;
				try{
					Random r = new Random();
					int waitTime = 5000;
					while(neighbors.isEmpty()){ //Case when no other clients are left in the system and wait to get from controller the recovered node
						
						System.out.println("No neighbors");
						Thread.sleep(waitTime);
						try{
							controller = new Socket(addr, port); //Create a socket to the controller (address , port)
						}
						catch (ConnectException e) {
							//Controller has crashed, change to standby controller
							if(port == Integer.parseInt(args[1])){
								addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
								port = Integer.parseInt(args[3]);
							}
							else{
								addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
								port = Integer.parseInt(args[1]);
							}						
							controller = new Socket(addr, port);
						}
						out = new PrintWriter(controller.getOutputStream(),true); //Send to controller						
						out.println("ALIVE " + nodeID + " " + ss.getLocalPort());
						
						in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
						
						responseLine = in.readLine();						
						
						if(responseLine.equals("ACK")){
							waitTime += 500;
							continue;
						}
						
						String [] tStr = responseLine.split(" ");
						if(tStr[0].equals("recover")){ //Store recovered node's data
							recoveredNode = Integer.parseInt(tStr[3]);
							neighbors.add(new Address(tStr[1], Integer.parseInt(tStr[2]),Integer.parseInt(tStr[3])));
							prevCrashed = -1;
						}						
					}
					
					index = r.nextInt(neighbors.size());
					neighborSock = new Socket(InetAddress.getByName(neighbors.get(index).getAddress()),neighbors.get(index).getPort());
					System.out.println("Node " + nodeID + " sending potato to " + neighbors.get(index).getID());
					
					outSock = new PrintWriter(neighborSock.getOutputStream(),true);
					
					if(prevCrashed == -1)
						outSock.println("potato 0");
					else
						outSock.println("crashed " + prevCrashed + " 0");
					
					prevCrashed = -1;
					
					inSock = new BufferedReader(new InputStreamReader(neighborSock.getInputStream())); 
				
					responseLine = inSock.readLine();
					
					if(responseLine.equals("ACK")){
						System.out.println("Potato sent\n");
					}
					
					neighborSock.close();
					outSock.close();
					break;
				
				}catch(SocketTimeoutException e){
					timeout = true;
				
				}catch(SocketException e){
					timeout = true;
				}
					
				if(timeout){						
					neighborSock.close();
					outSock.close();						
					timeout = false;
					prevCrashed = neighbors.get(index).getID();
					neighbors.remove(index);
					System.out.println(neighbors.size());
					
					//inform CONTROLLER for CRASHED NODE
					controller = new Socket(addr, port); //Create a socket to the controller (address , port)
					in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
					out = new PrintWriter(controller.getOutputStream(),true); //Send to controller
					
					//inform server for crashed node
					out.println("crashed " + prevCrashed + " " + nodeID + " " + ss.getLocalPort());
					responseLine = in.readLine();
					if(responseLine.equals("ACK"))
						System.out.println("Node " + nodeID + ": told server about failure at node " + prevCrashed);
					
					//controller.close();
					continue;
				}				
							
			} //end of while node == 0
			
			int previous = -1;
			
			while(true){
				Thread.sleep(2000);
				int index = -1;
				System.out.println(neighbors.size());
				
				if(hasPotato){
					try{
						Random r = new Random();
						int waitTime = 5000;
						while(neighbors.isEmpty()){ //Case when no other clients are left in the system and wait to get from controller the recovered node
							
							System.out.println("No neighbors");
							Thread.sleep(waitTime);
							try{
								controller = new Socket(addr, port); //Create a socket to the controller (address , port)
							}
							catch (ConnectException e) {
								//Controller has crashed, change to standby controller
								if(port == Integer.parseInt(args[1])){
									addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
									port = Integer.parseInt(args[3]);
								}
								else{
									addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
									port = Integer.parseInt(args[1]);
								}						
								controller = new Socket(addr, port);
							}
							out = new PrintWriter(controller.getOutputStream(),true); //Send to controller						
							out.println("ALIVE " + nodeID + " " + ss.getLocalPort());
							
							in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
							
							responseLine = in.readLine();						
							
							if(responseLine.equals("ACK")){
								waitTime += 500;
								continue;
							}
							
							String [] tStr = responseLine.split(" ");
							if(tStr[0].equals("receive")){
								while (true){
									responseLine = in.readLine();
									System.out.println(responseLine);
									if (responseLine.equals("done")) 
										break;
									
									String res[] = responseLine.split(" ");
									if(getIndex(Integer.parseInt(res[2]),neighbors) > 0){ //if already in list
										continue;
									}
									neighbors.add(new Address(res[0], Integer.parseInt(res[1]),Integer.parseInt(res[2]))); // store it in neighbors list		
							    }
								break;
							}
							
							else if(tStr[0].equals("recover")){ //Store recovered node's data
								recoveredNode = Integer.parseInt(tStr[3]);
								neighbors.add(new Address(tStr[1], Integer.parseInt(tStr[2]),Integer.parseInt(tStr[3])));
								prevCrashed = -1;
							}						
						}
						
						//Flooding for recovered node
						if(recoveredNode != -1){
							for(Address a : neighbors){
								neighborSock = new Socket(InetAddress.getByName(a.getAddress()),a.getPort());
								Address ad = neighbors.get(getIndex(recoveredNode, neighbors));
								
								System.out.println("Node " + nodeID + " floods for recovered node " + ad.getID());
								
								outSock = new PrintWriter(neighborSock.getOutputStream(),true);
								outSock.println("flood " + ad.getAddress() + " " + ad.getPort() + " " + ad.getID());
								
								inSock = new BufferedReader(new InputStreamReader(neighborSock.getInputStream())); 						
												
								responseLine = inSock.readLine(); //waiting to get ACK
								if(responseLine.equals("ACK")){
									System.out.println("Flood received correctly\n");
								}
								neighborSock.close();
							}
							
							try{
								controller = new Socket(addr, port); //Create a socket to the controller (address , port)
							}
							catch (ConnectException e) {
								//Controller has crashed, change to standby controller
								if(port == Integer.parseInt(args[1])){
									addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
									port = Integer.parseInt(args[3]);
								}
								else{
									addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
									port = Integer.parseInt(args[1]);
								}						
								controller = new Socket(addr, port);
							}
							out = new PrintWriter(controller.getOutputStream(),true); //Send to controller						
							out.println("FLOODDONE " + recoveredNode + " " + nodeID + " " + ss.getLocalPort());
							recoveredNode = -1;
						}
						
						//Choose randomly a node to send potato
						index = r.nextInt(neighbors.size());
						
						neighborSock = new Socket(InetAddress.getByName(neighbors.get(index).getAddress()),neighbors.get(index).getPort());
						outSock = new PrintWriter(neighborSock.getOutputStream(),true);
						
						System.out.println("Node " + nodeID + " sending potato to " + neighbors.get(index).getID());
						
						if(prevCrashed == -1){ //no previously crashed node known
							outSock.println("potato" + " " + nodeID);
						}
						else{ //previously crashed node
							outSock.println("crashed " + prevCrashed + " " + nodeID );
							prevCrashed = -1;
						}
												
						inSock = new BufferedReader(new InputStreamReader(neighborSock.getInputStream())); 
						
						neighborSock.setSoTimeout(7000); //set a timeout before read						
										
						responseLine = inSock.readLine(); //waiting to get ACK
						if(responseLine.equals("ACK")){
							System.out.println("Potato sent\n");
						}
						
						neighborSock.close();
						outSock.close();
						hasPotato = false;
						timeout = false;
						
					}catch(SocketTimeoutException e){
						timeout = true;
					
					}catch(SocketException e){
						timeout = true;
					}
						
					if(timeout){						
						neighborSock.close();
						outSock.close();						
						timeout = false;
						prevCrashed = neighbors.get(index).getID();
						neighbors.remove(index);
						System.out.println(neighbors.size());
						
						//inform CONTROLLER for CRASHED NODE
						try{
							controller = new Socket(addr, port); //Create a socket to the controller (address , port)
						}
						catch (ConnectException e) {
							//Controller has crashed, change to standby controller
							if(port == Integer.parseInt(args[1])){
								addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
								port = Integer.parseInt(args[3]);
							}
							else{
								addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
								port = Integer.parseInt(args[1]);
							}						
							controller = new Socket(addr, port);
						}
						in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
						out = new PrintWriter(controller.getOutputStream(),true); //Send to controller
						
						//inform server for crashed node
						out.println("crashed " + prevCrashed + " " + nodeID + " " + ss.getLocalPort());
						responseLine = in.readLine();
						if(responseLine.equals("ACK"))
							System.out.println("Node " + nodeID + ": told server about failure at node " + prevCrashed);
						
						//controller.close();
						continue;
					}				
					
				}
				else{ //receive potato
					neighborSock = ss.accept(); //wait connection from neighbor
					
					inSock = new BufferedReader(new InputStreamReader(neighborSock.getInputStream()));
					outSock = new PrintWriter(neighborSock.getOutputStream(),true);
					
					//Read message
					responseLine = inSock.readLine();
					System.out.println(responseLine);
					line = responseLine.split(" ");
					
					if(line[0].equals("force")){
						hasPotato = true;
						continue;
					}
					else if(line[0].equals("flood")){
						outSock.println("ACK");
						
						int r = Integer.parseInt(line[3]);
						if(r != nodeID){//ignore flood
							if(getIndex(r, neighbors) >= 0){ //already in the list
								neighbors.remove(getIndex(r, neighbors));
							}
							neighbors.add(new Address(line[1],Integer.parseInt(line[2]),r));	
							System.out.println("Node " + r + " recovered");							
						}
						continue; //loop without doing anything
					}
					//if got a crash message remove faulty client from list
					else if(line[0].equals("crashed")){
						int f = Integer.parseInt(line[1]);
						previous = Integer.parseInt(line[2]);
						if(f != nodeID){
							prevCrashed = f;
							int i;
							for(i = 0; i < neighbors.size(); i++){
								if(neighbors.get(i).getID() == f){
									neighbors.remove(i);
									System.out.println("Crashed node " + f + " removed");
									break;
								}
							}
							if(i == neighbors.size()){ //crashed node not found in list
								prevCrashed = -1;
							}
						}
					}
					else if(line[0].equals("recover")){
						int r = Integer.parseInt(line[3]);
						previous = Integer.parseInt(line[4]);
						if(r != nodeID){
							recoveredNode = r;
							int i;
							for(i = 0; i < neighbors.size(); i++){
								if(neighbors.get(i).getID() == r)
									break;
							}
							
							if(i == neighbors.size()){ //if node not found
								neighbors.add(new Address(line[1],Integer.parseInt(line[2]),r));	
								System.out.println("Node " + r + " recovered");
							}
							else // if already in list
								recoveredNode = -1;
						}
					}
					
					else{ //only potato
						previous = Integer.parseInt(line[1]);
					}
					
					System.out.println("Node " + nodeID + " got potato");
					state++;	
					System.out.println("CRASH ME NOW");
					Thread.sleep(5000);
					outSock.println("ACK");
					hasPotato = true;
					
					neighborSock.close();
					inSock.close();
					outSock.close();					
					
					//send info to CONTROLLER
					try{
						controller = new Socket(addr, port); //Create a socket to the controller (address , port)
					} 
					catch (ConnectException e) {
						//Controller has crashed, change to standby controller
						if(port == Integer.parseInt(args[1])){
							addr = InetAddress.getByName(args[2]); //IP @ of standby controller	
							port = Integer.parseInt(args[3]);
						}
						else{
							addr = InetAddress.getByName(args[0]); //IP @ of standby controller	
							port = Integer.parseInt(args[1]);
						}						
						controller = new Socket(addr, port);
					}
					
					in = new BufferedReader(new InputStreamReader(controller.getInputStream())); //Read from controller
					out = new PrintWriter(controller.getOutputStream(),true); //Send to controller
					
					out.println("update " + nodeID + " " + state + " " + ss.getLocalPort());
					responseLine = in.readLine();
					String [] str = responseLine.split(" ");
					
					if(responseLine.equals("ACK"))
						System.out.println("Node " + nodeID + ": Update sent");
					else if(str[0].equals("recover")){						
						recoveredNode = Integer.parseInt(str[3]);
						System.out.println("Received recover from controller and node "+ recoveredNode + " was recovered");
						neighbors.add(new Address(str[1], Integer.parseInt(str[2]),Integer.parseInt(str[3])));						
					}
				}				
			}	
			
		} catch (Exception e){
			e.printStackTrace();
		}	
	}
	
	public static int getIndex(int node, List<Address> list){
		for(int i = 0; i<list.size(); i++ ){
			if(node == list.get(i).getID())
				return i;
		}
		return -1;
	}
}

class Address{
	
	private String address; //IP address of client
	private int port; //Port that client listens from
	private int nodeId; //Node ID of the client
	
	public Address(String a, int p, int n){
		address = a;
		port = p;
		nodeId = n;
	}
	
	public String getAddress(){return address;}
	public int getPort(){return port;}
	public int getID(){return nodeId;}
	
}
